﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using IBNet.UtilsDataStructuresAndExtensions;
using IBNet;

namespace IBNet.DayTrade
{
   [DebuggerDisplay("FullSymbol: {FullSymbol}")]
   public class PriceBarsAndIndicatorValues
   {
      #region Fields

      private readonly Contract _contract;
      private readonly int _priceBarCapacity;
      private readonly int _secondsPerPriceBar;
      private readonly int _numberOf5SecBarsPerCompletePriceBar;

      private readonly FixedLengthCollection<PriceBar> _priceBars;
      private readonly IList<Indicator> _indicatorFunctions;
      private readonly FixedLengthCollection<decimal?>[] _indicatorValues;
      private readonly Dictionary<string, FixedLengthCollection<decimal?>>
         _indicatorNamesToIndicatorValues;

      private readonly TimeSpan _durationOfDataToRequestWhenBackfilling;
      private readonly HistoricalBarSize _historicalBarSizeWhenBackfilling;

      private PriceBar.Builder _bufferBar;
      private int _numberOf5SecBarsInBufferBar;
      private PriceBar _previousBar;
      private bool _isInNeedOfBackFill;

      #endregion

      #region Constructor

      /// <summary>
      /// Creates a new PriceBarsAndIndicatorValues.
      /// </summary>
      /// <param name="contract">The contract the PriceBarsAndIndicatorValues is based on.</param>
      /// <param name="numberOfPriceBarsToStore">The number of price bars to store.</param>
      /// <param name="secondsPerPriceBar">The seconds per price bar.</param>
      /// <param name="indicators">Indicators for this PriceBarsAndIndicatorValues. 
      /// Specify null for no indicators.</param>
      public PriceBarsAndIndicatorValues(Contract contract, int numberOfPriceBarsToStore,
         RealTimeBarSeconds secondsPerPriceBar, IList<Indicator> indicators)
      {
         ConfirmConstructorArgumentsAreGood(
            contract, numberOfPriceBarsToStore, secondsPerPriceBar);

         _contract = contract;
         _priceBarCapacity = numberOfPriceBarsToStore;
         _secondsPerPriceBar = (int)secondsPerPriceBar;
         _numberOf5SecBarsPerCompletePriceBar = _secondsPerPriceBar / 5;

         _isInNeedOfBackFill = true;
         // + 1 to ensure that the returned data doesn't include a "non-round"
         // time. For example, a non-round time when requesting 5-second data is
         // :03 and round times are multiples of :05.
         int secondsOfDataToRequestWhenBackfilling =
            _secondsPerPriceBar * (_priceBarCapacity + 1);
         _durationOfDataToRequestWhenBackfilling =
            TimeSpan.FromSeconds(secondsOfDataToRequestWhenBackfilling);
         _historicalBarSizeWhenBackfilling = 
            secondsPerPriceBar.ConvertToHistoricalBarSize();

         _priceBars = new FixedLengthCollection<PriceBar>(_priceBarCapacity);
         _bufferBar = new PriceBar.Builder();
         
         if (indicators != null && indicators.Count > 0)
         {
            _indicatorFunctions = indicators;
            _indicatorValues = new FixedLengthCollection<decimal?>[indicators.Count];
            _indicatorNamesToIndicatorValues =
               new Dictionary<string, FixedLengthCollection<decimal?>>();
            for (int i = 0; i < indicators.Count; i++)
            {
               _indicatorValues[i] 
                  = new FixedLengthCollection<decimal?>(numberOfPriceBarsToStore);
               _indicatorNamesToIndicatorValues.Add(
                  indicators[i].Name, _indicatorValues[i]);
            }
         }
      }

      private static void ConfirmConstructorArgumentsAreGood(Contract contract,
         int numberOfPriceBarsToStore, RealTimeBarSeconds secondsPerPriceBar)
      {
         if (contract == null)
            throw new ArgumentNullException("contract");
         if (numberOfPriceBarsToStore <= 0)
            throw new ArgumentOutOfRangeException(
               "numberOfPriceBarsToStore", 
               "numberOfPriceBarsToStore must be greater than or equal to 1.");
         switch (secondsPerPriceBar)
         {
            case RealTimeBarSeconds.Five:
            case RealTimeBarSeconds.Fifteen:
            case RealTimeBarSeconds.Thirty:
            case RealTimeBarSeconds.Sixty:
            case RealTimeBarSeconds.OneHundredTwenty:
            case RealTimeBarSeconds.EighteenHundred:
            case RealTimeBarSeconds.ThirtySixHundred:
            break;
            default:
            throw new ArgumentOutOfRangeException("secondsPerPriceBar");
         }
      }

      #endregion // Constructor

      #region Properties

      public string FullSymbol { get { return _contract.FullSymbol; } }
      public Contract Contract { get { return _contract; } }

      public bool IsInNeedOfBackfill { get { return _isInNeedOfBackFill; } }
      public TimeSpan DurationOfDataToRequestWhenBackfilling
         { get { return _durationOfDataToRequestWhenBackfilling; } }
      public HistoricalBarSize HistoricalBarSizeWhenBackfilling
         { get { return _historicalBarSizeWhenBackfilling; } }

      public int PriceBarCapacity { get { return _priceBarCapacity; } }
      public int SecondsPerPriceBar { get { return _secondsPerPriceBar; } }      
      public ReadOnlyFixedLengthCollection<PriceBar> PriceBars
         { get { return _priceBars.AsReadOnly(); } }

      public ReadOnlyFixedLengthCollection<decimal?> this[string indicatorName]
      {
         get
         {
            ReadOnlyFixedLengthCollection<decimal?> indicatorValues =
               _indicatorNamesToIndicatorValues[indicatorName].AsReadOnly();
            return indicatorValues;
         }
      }

      #endregion // Properties

      #region Public Methods

      /// <summary>
      /// Push-fronts a PriceBar.
      /// </summary>
      /// <param name="priceBar">A PriceBar to push-front.</param>
      public void PushFrontFiveSecondBar(PriceBar priceBar)
      {
         if (PriceBarHasTimeGapBetweenItAndPreviousPriceBar(priceBar))
         {
            if (HaveAtLeastOnePriceBarAvailableToMakeATimeGapFillingBar())
            {
               PriceBar fillerBarForFiveSecondsAgo 
                  = NewFillerBarForFiveSecondsAgo(priceBar);
               PushFrontFiveSecondBar(fillerBarForFiveSecondsAgo);
            }
         }

         _previousBar = priceBar;

         if (_numberOf5SecBarsInBufferBar == 0)
         {
            if (PriceBarIsBarStarting(priceBar))
            {
               InitializeBufferBarWithFirstPriceBar(priceBar);
            }
            else
            {
               return;
            }
         }
         else
         {
            UpdateBufferBarHighLowCloseVolumeWithNewPriceBar(priceBar);
         }

         if (_numberOf5SecBarsInBufferBar == _numberOf5SecBarsPerCompletePriceBar)
         {
            PriceBar completePriceBar = _bufferBar.Build();
            _priceBars.PushFront(completePriceBar);
            CalculateAndPushFrontIndicatorValues();
            ResetBufferBar();
         }
      }

      /// <summary>
      /// Push-fronts a back fill PriceBar.
      /// </summary>
      /// <param name="backFillBar">A back fill PriceBar.</param>
      public void PushFrontBackFillBar(PriceBar backFillBar)
      {
         _priceBars.PushFront(backFillBar);
         CalculateAndPushFrontIndicatorValues();

         //if (BackfillIsComplete())
         if (_priceBars.Count == _priceBars.Length)
         {
            _previousBar = new PriceBar(-1, -1, -1, -1, -1,
               _priceBars[0].OpenTimeSince1970 + _secondsPerPriceBar - 5, DateTime.Now);
            _isInNeedOfBackFill = false;
         }
      }

      //private bool BackfillIsComplete()
      //{
      //   if (_priceBars.Count == _priceBars.Length)
      //   {
      //      // When the first real-time non-backfill price bar arrives
      //      // it will have an open time equal to 
      //      // _priceBars[0].OpenTimeSince1970 + _secondsPerPriceBar.
      //      // Therefore, set _previousOpenTime to
      //      // _priceBars[0].OpenTimeSince1970 + _secondsPerPriceBar - 5
      //      _previousOpenTime 
      //         = _priceBars[0].OpenTimeSince1970 + _secondsPerPriceBar - 5;
      //      return true;            
      //   }
      //   else
      //   {
      //      return false;
      //   }
      //}

      public bool OpenTimeIsAtLeastFiveSecondsAheadOfPreviousOpenTime(
         long priceBarOpenTime)
      {
         // Upon backfilling, priceBarOpenTime will likely be less than previousTime_ 
         // for 1 or 2 bars. When priceBarOpenTime is 5 seconds ahead of previousTime_
         // then things can proceed.
         //if (priceBarOpenTime <= _previousOpenTime)
         if (priceBarOpenTime > _previousBar.OpenTimeSince1970)
         {
            return true;
         }
         else
         {
            return false;
         }
      }

      /// <summary>
      /// Determines whether a price bar is a bar-starting price bar.
      /// Bar-starting price bars have open times that are multiples of :(SecondsPerPriceBar).
      /// </summary>
      /// <remarks>
      /// Bar-starting open times for 15-second bars are
      /// multiples of :15 -> 9:30:00, 9:30:15, 9:30:30, 9:30:45.
      /// Bar-starting open times for 60-second bars are
      /// multiples of 1:00 -> 9:30:00, 9:31:00, 9:32:00, 9:33:00.
      /// Bar-starting open times for 120-second bars are
      /// multiples of 2:00 -> 9:30:00, 9:32:00, 9:34:00, 9:36:00.
      /// </remarks>
      /// <param name="priceBar">A PriceBar to determine whether it is a bar-starting 
      /// price bar.</param>
      /// <returns>true if the price bar is a bar-starting price bar; otherwise, false.</returns>
      public bool PriceBarIsBarStarting(PriceBar priceBar)
      {
         bool priceBarIsABarStarting =
            priceBar.OpenTimeSince1970 % _secondsPerPriceBar == 0;
         return priceBarIsABarStarting;
      }

      /// <summary>
      /// Determines whether a price bar a bar-completing price bar. 
      /// </summary>
      /// <remarks>
      /// Bar-completing price bars have opening times 
      /// that equal (multiple of :(SecondsPerPriceBar)) - 5.
      /// Bar-completing open times for 15-second bars are
      /// (multiple of :15) - 5 -> 9:30:10, 9:30:25, 9:30:40, 9:30:55.
      /// Bar-completing open times for 60-second bars are
      /// (multiple of 1:00) - 5 -> 9:30:55, 9:31:55, 9:32:55, 9:33:55.
      /// Bar-completing open times for 120-second bars are
      /// (multiple of 2:00) - 5 -> 9:31:55, 9:33:55, 9:35:55, 9:37:55.
      /// </remarks>
      /// <param name="priceBar">A PriceBar to determine whether it is a bar-completing
      /// price bar.</param>
      /// <returns>true if the open time is a bar-completing open time; otherwise, false.</returns>
      public bool PriceBarIsBarCompleting(PriceBar priceBar)
      {
         bool priceBarIsBarCompleting =
            (priceBar.OpenTimeSince1970 + 5) % _secondsPerPriceBar == 0;
         return priceBarIsBarCompleting;
      }

      #endregion // Public Methods

      #region Private Methods

      private PriceBar NewFillerBarForFiveSecondsAgo(
         PriceBar priceBarWithTimeGapBetweenItAndPreviousPriceBar)
      {
         long fillerBarOpenTimeSince1970 
            = priceBarWithTimeGapBetweenItAndPreviousPriceBar.OpenTimeSince1970;

         DateTime fillerBarOpenTime = fillerBarOpenTimeSince1970.ConvertTo1970DateTime();

         //PriceBar fillerBarForFiveSecondsAgo = new PriceBar(
         //   _previousClose,
         //   _previousClose,
         //   _previousClose,
         //   _previousClose,
         //   0,
         //   fillerBarOpenTimeSince1970,
         //   fillerBarOpenTime);

         PriceBar fillerBarForFiveSecondsAgo = new PriceBar(
            _previousBar.Close,
            _previousBar.Close,
            _previousBar.Close,
            _previousBar.Close,
            0,
            fillerBarOpenTimeSince1970,
            fillerBarOpenTime);

         return fillerBarForFiveSecondsAgo;
      }

      private bool PriceBarHasTimeGapBetweenItAndPreviousPriceBar(PriceBar priceBar)
      {
         bool priceBarHasTimeGapBetweenItAndPreviousPriceBar
            = (priceBar.OpenTimeSince1970 - _previousBar.OpenTimeSince1970) >= 10;
         return priceBarHasTimeGapBetweenItAndPreviousPriceBar;

      }

      private bool HaveAtLeastOnePriceBarAvailableToMakeATimeGapFillingBar()
      {
         bool haveAtLeastOnePriceBarAvailableToMakeAFillerBarFrom =
            (_priceBars.Count >= 1 || _numberOf5SecBarsInBufferBar >= 1);
         return haveAtLeastOnePriceBarAvailableToMakeAFillerBarFrom;
      }

      private void UpdateBufferBarHighLowCloseVolumeWithNewPriceBar(PriceBar priceBar)
      {
         if (priceBar.High > _bufferBar.High)
         {
            _bufferBar.High = priceBar.High;
         }
         if (priceBar.Low < _bufferBar.Low)
         {
            _bufferBar.Low = priceBar.Low;
         }
         _bufferBar.Close = priceBar.Close;
         _bufferBar.Volume += priceBar.Volume;
         _numberOf5SecBarsInBufferBar++;
      }

      private void InitializeBufferBarWithFirstPriceBar(PriceBar firstPriceBar)
      {
         _bufferBar.Open = firstPriceBar.Open;
         _bufferBar.High = firstPriceBar.High;
         _bufferBar.Low = firstPriceBar.Low;
         _bufferBar.Close = firstPriceBar.Close;
         _bufferBar.Volume = firstPriceBar.Volume;
         _bufferBar.LongOpenTime = firstPriceBar.OpenTimeSince1970;
         _bufferBar.OpenTime = firstPriceBar.OpenTimeSince1970.ConvertTo1970DateTime();
         _numberOf5SecBarsInBufferBar++;
      }

      private void ResetBufferBar()
      {
         _bufferBar = new PriceBar.Builder();
         _numberOf5SecBarsInBufferBar = 0;
      }

      private void CalculateAndPushFrontIndicatorValues()
      {
         for (int i = 0; i < _indicatorFunctions.Count; i++)
         {
            Indicator ithIndicator = _indicatorFunctions[i];
            decimal? indicatorValue = ithIndicator.CalculateIndicator(_priceBars);
            _indicatorValues[i].PushFront(indicatorValue);
         }
      }

      #endregion // Private Methods
   }
}
